package io.github.bigbird0101.youyishi.code.compile.support;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference;
import io.github.bigbird0101.youyishi.code.CodeContent;
import io.github.bigbird0101.youyishi.code.JavaCodeInfo;
import io.github.bigbird0101.youyishi.code.compile.CodeMetadata;
import io.github.bigbird0101.youyishi.code.compile.Compiler;
import javassist.*;
import javassist.bytecode.AnnotationsAttribute;
import javassist.bytecode.ClassFile;
import javassist.bytecode.ConstPool;
import javassist.bytecode.ParameterAnnotationsAttribute;
import javassist.bytecode.annotation.Annotation;
import javassist.bytecode.annotation.ArrayMemberValue;
import javassist.bytecode.annotation.StringMemberValue;
import lombok.extern.slf4j.Slf4j;

import java.security.ProtectionDomain;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

/**
 * @author m15179835326@163.com
 */
@Slf4j
public class JavassistCompiler implements Compiler {
        @Override
        public boolean match(List<CodeContent<?>> codeContentsList) {
            return codeContentsList.stream().noneMatch(s-> s.getContent() instanceof String);
        }

        @Override
        public List<CodeMetadata> compileCode(List<CodeContent<?>> codeContentsList) {
            return codeContentsList.stream().map(this::buildAndConvert).collect(Collectors.toList());
        }

        private CodeMetadata buildAndConvert(CodeContent<?> codeContent){
            try {
                Object content = codeContent.getContent();
                JavaCodeInfo javaCodeInfo = JSONObject.parseObject(JSONObject.toJSONString(content), new TypeReference<JavaCodeInfo>() {
                });
                ClassPool pool = new ClassPool(true);
                pool.appendClassPath(new LoaderClassPath(getClass().getClassLoader()));
                CtClass ctClass;
                if(null!=javaCodeInfo.getIsInterface()&&javaCodeInfo.getIsInterface()){
                    ctClass = pool.makeInterface(javaCodeInfo.getClassName(), StrUtil.isNotBlank(javaCodeInfo.getSuperClass())?
                            pool.get(javaCodeInfo.getSuperClass()):null);
                }else{
                    ctClass = pool.makeClass(javaCodeInfo.getClassName(),StrUtil.isNotBlank(javaCodeInfo.getSuperClass())?
                            pool.get(javaCodeInfo.getSuperClass()):null);
                }
                ClassFile classFile = ctClass.getClassFile();
                ConstPool constPool = classFile.getConstPool();
                // add class annotation
                List<JavaCodeInfo.AnnotationInfo<?>> classAnnotations = javaCodeInfo.getClassAnnotations();
                if(CollectionUtil.isNotEmpty(classAnnotations)){
                    AnnotationsAttribute annotationsAttribute = getAnnotationsAttribute(constPool, classAnnotations);
                    classFile.addAttribute(annotationsAttribute);
                }

                // add imported packages
                if(CollectionUtil.isNotEmpty(javaCodeInfo.getImports())) {
                    javaCodeInfo.getImports().forEach(pool::importPackage);
                }

                // add implemented interfaces
                if(CollectionUtil.isNotEmpty(javaCodeInfo.getImplInterfaces())) {
                    for (String iface : javaCodeInfo.getImplInterfaces()) {
                        ctClass.addInterface(pool.get(iface));
                    }
                }


                // add fields
                if(CollectionUtil.isNotEmpty(javaCodeInfo.getFields())) {
                    for (String field : javaCodeInfo.getFields()) {
                        ctClass.addField(CtField.make(field, ctClass));
                    }
                }

                // add constructors
                if(CollectionUtil.isNotEmpty(javaCodeInfo.getConstructors())) {
                    for (String constructor : javaCodeInfo.getConstructors()) {
                        ctClass.addConstructor(CtNewConstructor.make(constructor, ctClass));
                    }
                }

                // add methods
                addMethods(javaCodeInfo, pool, ctClass, constPool);
                //使用spring的类加载器
                ProtectionDomain protectionDomain = getClass().getProtectionDomain();
                Class<?> aClass = ctClass.toClass(getClass().getClassLoader(), protectionDomain);
                return new CodeMetadata(codeContent.getClassName(),aClass,codeContent);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

    private void addMethods(JavaCodeInfo javaCodeInfo, ClassPool pool, CtClass ctClass, ConstPool constPool) throws NotFoundException, CannotCompileException {
        if(CollectionUtil.isNotEmpty(javaCodeInfo.getMethods())) {
            for (JavaCodeInfo.MethodInfo method : javaCodeInfo.getMethods()) {
                AnnotationsAttribute annotationsAttribute = new AnnotationsAttribute(constPool, AnnotationsAttribute.visibleTag);
                CtMethod ctMethod;
                if(null!= javaCodeInfo.getIsInterface()&& javaCodeInfo.getIsInterface()){
                    CtClass[] paramClass = getCtClasses(pool, method.getParamClassNames());
                    CtClass[] exceptionClass = getCtClasses(pool, method.getExceptionClassNames());
                    ctMethod=CtNewMethod.abstractMethod(pool.get(method.getMethodReturnTypeClassName()),method.getMethodName(),paramClass,exceptionClass, ctClass);
                }else{
                    ctMethod = CtNewMethod.make(method.getMethodContent(), ctClass);
                }
                List<JavaCodeInfo.AnnotationInfo<?>> annotationAttributes = method.getAnnotationInfos();
                if(CollectionUtil.isNotEmpty(annotationAttributes)) {
                    annotationAttributes.forEach(annotationInfoParam -> {
                        Annotation annotation= buildAnnotation(constPool, annotationInfoParam);
                        annotationsAttribute.addAnnotation(annotation);
                    });
                    ctMethod.getMethodInfo().addAttribute(annotationsAttribute);
                }

                List<List<JavaCodeInfo.AnnotationInfo<?>>> paramAnnotationInfos = method.getParamAnnotationInfos();
                if(CollectionUtil.isNotEmpty(paramAnnotationInfos)){
                    int size = paramAnnotationInfos.size();
                    Annotation[][] annotations = new Annotation[size][];
                    for(int a=0;a<size;a++){
                        int length = paramAnnotationInfos.get(a).size();
                        annotations[a] = new Annotation[length];
                        for(int b=0;b<length;b++){
                            JavaCodeInfo.AnnotationInfo<?> annotationInfo = paramAnnotationInfos.get(a).get(b);
                            Annotation annotation= buildAnnotation(constPool, annotationInfo);
                            annotations[a][b]=annotation;
                        }
                    }
                    ParameterAnnotationsAttribute parameterAnnotationsAttribute=new ParameterAnnotationsAttribute(constPool, ParameterAnnotationsAttribute.visibleTag);
                    parameterAnnotationsAttribute.setAnnotations(annotations);
                    ctMethod.getMethodInfo().addAttribute(parameterAnnotationsAttribute);
                }
                ctClass.addMethod(ctMethod);
            }
        }
    }

    private Annotation buildAnnotation(ConstPool constPool, JavaCodeInfo.AnnotationInfo<?> annotationInfo) {
        String annotationName = annotationInfo.getAnnotationName();
        String property = annotationInfo.getProperty();
        String valueType = annotationInfo.getValueType();
        Object annotationValue = annotationInfo.getAnnotationValue();
        Annotation annotation = new Annotation(annotationName, constPool);
        if (StrUtil.isNotBlank(property)) {
            if("String".equals(valueType)){
                annotation.addMemberValue(property,new StringMemberValue((String) annotationValue,constPool));
            }else if("Array".equals(valueType)){
                ArrayMemberValue value = new ArrayMemberValue(constPool);
                List<String> an= (List<String>) annotationValue;
                StringMemberValue[] objects = an.stream()
                        .map(s -> new StringMemberValue(s, constPool))
                        .toArray(StringMemberValue[]::new);
                value.setValue(objects);
                annotation.addMemberValue(property, value);
            }
        }
        return annotation;
    }

    private AnnotationsAttribute getAnnotationsAttribute(ConstPool constPool, List<JavaCodeInfo.AnnotationInfo<?>> classAnnotations) {
        AnnotationsAttribute annotationsAttribute = new AnnotationsAttribute(constPool, AnnotationsAttribute.visibleTag);
        classAnnotations.forEach(annotationInfoParam -> {
            Annotation annotation=buildAnnotation(constPool, annotationInfoParam);
            annotationsAttribute.addAnnotation(annotation);
        });
        return annotationsAttribute;
    }

    private CtClass[] getCtClasses(ClassPool pool, List<String> classNames) {
            if(CollectionUtil.isEmpty(classNames)){
                return null;
            }
            return Optional.of(classNames)
                    .stream()
                    .flatMap(Collection::stream)
                    .map(s -> {
                        try {
                            return pool.get(s);
                        } catch (NotFoundException e) {
                            throw new RuntimeException(e);
                        }
                    }).toArray(CtClass[]::new);
    }

}